/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.transforms.display;



import com.fasterxml.jackson.annotation.JsonGetter;

import com.fasterxml.jackson.annotation.JsonIgnore;

import com.fasterxml.jackson.annotation.JsonInclude;

import com.fasterxml.jackson.annotation.JsonValue;

import com.google.auto.value.AutoValue;

import com.bff.gaia.unified.sdk.options.ValueProvider;

import com.bff.gaia.unified.sdk.transforms.PTransform;

import com.bff.gaia.unified.vendor.guava.com.google.common.base.Joiner;

import com.bff.gaia.unified.vendor.guava.com.google.common.collect.ImmutableList;

import com.bff.gaia.unified.vendor.guava.com.google.common.collect.ImmutableMap;

import com.bff.gaia.unified.vendor.guava.com.google.common.collect.Maps;

import com.bff.gaia.unified.vendor.guava.com.google.common.collect.Sets;

import org.joda.time.Duration;

import org.joda.time.Instant;

import org.joda.time.format.DateTimeFormatter;

import org.joda.time.format.ISODateTimeFormat;



import javax.annotation.Nullable;

import java.io.Serializable;

import java.util.*;



import static com.bff.gaia.unified.vendor.guava.com.google.common.base.Preconditions.checkArgument;

import static com.bff.gaia.unified.vendor.guava.com.google.common.base.Preconditions.checkNotNull;



/**

 * Static display data associated with a pipeline component. Display data is useful for pipeline

 * runner UIs and diagnostic dashboards to display details about {@link PTransform PTransforms} that

 * make up a pipeline.

 *

 * <p>Components specify their display data by implementing the {@link HasDisplayData} interface.

 */

public class DisplayData implements Serializable {

  private static final DisplayData EMPTY = new DisplayData(Maps.newHashMap());

  private static final DateTimeFormatter TIMESTAMP_FORMATTER = ISODateTimeFormat.dateTime();



  private final ImmutableMap<Identifier, Item> entries;



  private DisplayData(Map<Identifier, Item> entries) {

    this.entries = ImmutableMap.copyOf(entries);

  }



  /** Default empty {@link DisplayData} instance. */

  public static DisplayData none() {

    return EMPTY;

  }



  /**

   * Collect the {@link DisplayData} from a component. This will traverse all subcomponents

   * specified via {@link Builder#include} in the given component. Data in this component will be in

   * a namespace derived from the component.

   */

  public static DisplayData from(HasDisplayData component) {

    checkNotNull(component, "component argument cannot be null");



    InternalBuilder builder = new InternalBuilder();

    builder.include(Path.root(), component);



    return builder.build();

  }



  /**

   * Infer the {@link Type} for the given object.

   *

   * <p>Use this method if the type of metadata is not known at compile time. For example:

   *

   * <pre>{@code @Override

   * public void populateDisplayData(DisplayData.Builder builder) {

   *   Optional<DisplayData.Type> type = DisplayData.inferType(foo);

   *   if (type.isPresent()) {

   *     builder.add(DisplayData.item("foo", type.get(), foo));

   *   }

   * }

   * }</pre>

   *

   * @return The inferred {@link Type}, or null if the type cannot be inferred,

   */

  @Nullable

  public static Type inferType(@Nullable Object value) {

    return Type.tryInferFrom(value);

  }



  @JsonValue

  public Collection<Item> items() {

    return entries.values();

  }



  public Map<Identifier, Item> asMap() {

    return entries;

  }



  @Override

  public int hashCode() {

    return entries.hashCode();

  }



  @Override

  public boolean equals(Object obj) {

    if (obj instanceof DisplayData) {

      DisplayData that = (DisplayData) obj;

      return Objects.equals(this.entries, that.entries);

    }



    return false;

  }



  @Override

  public String toString() {

    StringBuilder builder = new StringBuilder();

    boolean isFirstLine = true;

    for (Item entry : entries.values()) {

      if (isFirstLine) {

        isFirstLine = false;

      } else {

        builder.append("\n");

      }



      builder.append(entry);

    }



    return builder.toString();

  }



  /** Utility to build up display data from a component and its included subcomponents. */

  public interface Builder {

    /**

     * Register display data from the specified subcomponent at the given path. For example, a

     * {@link PTransform} which delegates to a user-provided function can implement {@link

     * HasDisplayData} on the function and include it from the {@link PTransform}:

     *

     * <pre>{@code @Override

     * public void populateDisplayData(DisplayData.Builder builder) {

     *   super.populateDisplayData(builder);

     *

     *   builder

     *     // To register the class name of the userFn

     *     .add(DisplayData.item("userFn", userFn.getClass()))

     *     // To allow the userFn to register additional display data

     *     .include("userFn", userFn);

     * }

     * }</pre>

     *

     * <p>Using {@code include(path, subcomponent)} will associate each of the registered items with

     * the namespace of the {@code subcomponent} being registered, with the specified path element

     * relative to the current path. To register display data in the current path and namespace,

     * such as from a base class implementation, use {@code

     * subcomponent.populateDisplayData(builder)} instead.

     *

     * @see HasDisplayData#populateDisplayData(DisplayData.Builder)

     */

    Builder include(String path, HasDisplayData subComponent);



    /**

     * Register display data from the specified component on behalf of the current component.

     * Display data items will be added with the subcomponent namespace but the current component

     * path.

     *

     * <p>This is useful for components which simply wrap other components and wish to retain the

     * display data from the wrapped component. Such components should implement {@code

     * populateDisplayData} as:

     *

     * <pre>{@code @Override

     * public void populateDisplayData(DisplayData.Builder builder) {

     *   builder.delegate(wrapped);

     * }

     * }</pre>

     */

    Builder delegate(HasDisplayData component);



    /** Register the given display item. */

    Builder add(ItemSpec<?> item);



    /** Register the given display item if the value is not null. */

    Builder addIfNotNull(ItemSpec<?> item);



    /** Register the given display item if the value is different than the specified default. */

    <T> Builder addIfNotDefault(ItemSpec<T> item, @Nullable T defaultValue);

  }



  /**

   * {@link Item Items} are the unit of display data. Each item is identified by a given path, key,

   * and namespace from the component the display item belongs to.

   *

   * <p>{@link Item Items} are registered via {@link DisplayData.Builder#add} within {@link

   * HasDisplayData#populateDisplayData} implementations.

   */

  @AutoValue

  public abstract static class Item implements Serializable {



    /** The path for the display item within a component hierarchy. */

    @Nullable

    @JsonIgnore

    public abstract Path getPath();



    /**

     * The namespace for the display item. The namespace defaults to the component which the display

     * item belongs to.

     */

    @Nullable

    @JsonGetter("namespace")

    public abstract Class<?> getNamespace();



    /**

     * The key for the display item. Each display item is created with a key and value via {@link

     * DisplayData#item}.

     */

    @JsonGetter("key")

    public abstract String getKey();



    /**

     * Retrieve the {@link DisplayData.Type} of display data. All metadata conforms to a predefined

     * set of allowed types.

     */

    @JsonGetter("type")

    public abstract Type getType();



    /**

     * Retrieve the value of the display item. The value is translated from the input to {@link

     * DisplayData#item} into a format suitable for display. Translation is based on the item's

     * {@link #getType() type}.

     */

    @JsonGetter("value")

    public abstract Object getValue();



    /**

     * Return the optional short value for an item, or null if none is provided.

     *

     * <p>The short value is an alternative display representation for items having a long display

     * value. For example, the {@link #getValue() value} for {@link Type#JAVA_CLASS} items contains

     * the full class name with package, while the short value contains just the class name.

     *

     * <p>A {@link #getValue() value} will be provided for each display item, and some types may

     * also provide a short-value. If a short value is provided, display data consumers may choose

     * to display it instead of or in addition to the {@link #getValue() value}.

     */

    @JsonGetter("shortValue")

    @JsonInclude(JsonInclude.Include.NON_NULL)

    @Nullable

    public abstract Object getShortValue();



    /**

     * Retrieve the optional label for an item. The label is a human-readable description of what

     * the metadata represents. UIs may choose to display the label instead of the item key.

     *

     * <p>If no label was specified, this will return {@code null}.

     */

    @JsonGetter("label")

    @JsonInclude(JsonInclude.Include.NON_NULL)

    @Nullable

    public abstract String getLabel();



    /**

     * Retrieve the optional link URL for an item. The URL points to an address where the reader can

     * find additional context for the display data.

     *

     * <p>If no URL was specified, this will return {@code null}.

     */

    @JsonGetter("linkUrl")

    @JsonInclude(JsonInclude.Include.NON_NULL)

    @Nullable

    public abstract String getLinkUrl();



    private static Item create(ItemSpec<?> spec, Path path) {

      checkNotNull(spec, "spec cannot be null");

      checkNotNull(path, "path cannot be null");

      Class<?> ns = checkNotNull(spec.getNamespace(), "namespace must be set");



      return new AutoValue_DisplayData_Item(

          path,

          ns,

          spec.getKey(),

          spec.getType(),

          spec.getValue(),

          spec.getShortValue(),

          spec.getLabel(),

          spec.getLinkUrl());

    }



    @Override

    public String toString() {

      return String.format("%s%s:%s=%s", getPath(), getNamespace().getName(), getKey(), getValue());

    }

  }



  /**

   * Specifies an {@link Item} to register as display data. Each item is identified by a given path,

   * key, and namespace from the component the display item belongs to.

   *

   * <p>{@link Item Items} are registered via {@link DisplayData.Builder#add} within {@link

   * HasDisplayData#populateDisplayData} implementations.

   */

  @AutoValue

  public abstract static class ItemSpec<T> implements Serializable {

    /**

     * The namespace for the display item. If unset, defaults to the component which the display

     * item is registered to.

     */

    @Nullable

    public abstract Class<?> getNamespace();



    /**

     * The key for the display item. Each display item is created with a key and value via {@link

     * DisplayData#item}.

     */

    public abstract String getKey();



    /**

     * The {@link DisplayData.Type} of display data. All display data conforms to a predefined set

     * of allowed types.

     */

    public abstract Type getType();



    /**

     * The value of the display item. The value is translated from the input to {@link

     * DisplayData#item} into a format suitable for display. Translation is based on the item's

     * {@link #getType() type}.

     */

    @Nullable

    public abstract Object getValue();



    /**

     * The optional short value for an item, or {@code null} if none is provided.

     *

     * <p>The short value is an alternative display representation for items having a long display

     * value. For example, the {@link #getValue() value} for {@link Type#JAVA_CLASS} items contains

     * the full class name with package, while the short value contains just the class name.

     *

     * <p>A {@link #getValue() value} will be provided for each display item, and some types may

     * also provide a short-value. If a short value is provided, display data consumers may choose

     * to display it instead of or in addition to the {@link #getValue() value}.

     */

    @Nullable

    public abstract Object getShortValue();



    /**

     * The optional label for an item. The label is a human-readable description of what the

     * metadata represents. UIs may choose to display the label instead of the item key.

     */

    @Nullable

    public abstract String getLabel();



    /**

     * The optional link URL for an item. The URL points to an address where the reader can find

     * additional context for the display data.

     */

    @Nullable

    public abstract String getLinkUrl();



    private static <T> ItemSpec<T> create(String key, Type type, @Nullable T value) {

      return ItemSpec.<T>builder().setKey(key).setType(type).setRawValue(value).build();

    }



    /**

     * Set the item {@link ItemSpec#getNamespace() namespace} from the given {@link Class}.

     *

     * <p>This method does not alter the current instance, but instead returns a new {@link

     * ItemSpec} with the namespace set.

     */

    public ItemSpec<T> withNamespace(Class<?> namespace) {

      checkNotNull(namespace, "namespace argument cannot be null");

      return toBuilder().setNamespace(namespace).build();

    }



    /**

     * Set the item {@link Item#getLabel() label}.

     *

     * <p>Specifying a null value will clear the label if it was previously defined.

     *

     * <p>This method does not alter the current instance, but instead returns a new {@link

     * ItemSpec} with the label set.

     */

    public ItemSpec<T> withLabel(@Nullable String label) {

      return toBuilder().setLabel(label).build();

    }



    /**

     * Set the item {@link Item#getLinkUrl() link url}.

     *

     * <p>Specifying a null value will clear the link url if it was previously defined.

     *

     * <p>This method does not alter the current instance, but instead returns a new {@link

     * ItemSpec} with the link url set.

     */

    public ItemSpec<T> withLinkUrl(@Nullable String url) {

      return toBuilder().setLinkUrl(url).build();

    }



    /**

     * Creates a similar item to the current instance but with the specified value.

     *

     * <p>This should only be used internally. It is useful to compare the value of a {@link

     * DisplayData.Item} to the value derived from a specified input.

     */

    private ItemSpec<T> withValue(T value) {

      return toBuilder().setRawValue(value).build();

    }



    @Override

    public String toString() {

      return String.format("%s:%s=%s", getNamespace(), getKey(), getValue());

    }



    static <T> ItemSpec.Builder<T> builder() {

      return new AutoValue_DisplayData_ItemSpec.Builder<>();

    }



    abstract ItemSpec.Builder<T> toBuilder();



    @AutoValue.Builder

    abstract static class Builder<T> {

      public abstract ItemSpec.Builder<T> setKey(String key);



      public abstract ItemSpec.Builder<T> setNamespace(@Nullable Class<?> namespace);



      public abstract ItemSpec.Builder<T> setType(Type type);



      public abstract ItemSpec.Builder<T> setValue(@Nullable Object longValue);



      public abstract ItemSpec.Builder<T> setShortValue(@Nullable Object shortValue);



      public abstract ItemSpec.Builder<T> setLabel(@Nullable String label);



      public abstract ItemSpec.Builder<T> setLinkUrl(@Nullable String url);



      public abstract ItemSpec<T> build();



      abstract Type getType();



      ItemSpec.Builder<T> setRawValue(@Nullable T value) {

        FormattedItemValue formatted = getType().safeFormat(value);

        return this.setValue(formatted.getLongValue()).setShortValue(formatted.getShortValue());

      }

    }

  }



  /**

   * Unique identifier for a display data item within a component.

   *

   * <p>Identifiers are composed of:

   *

   * <ul>

   *   <li>A {@link #getPath() path} based on the component hierarchy

   *   <li>The {@link #getKey() key} it is registered with

   *   <li>A {@link #getNamespace() namespace} generated from the class of the component which

   *       registered the item.

   * </ul>

   *

   * <p>Display data registered with the same key from different components will have different

   * namespaces and thus will both be represented in the composed {@link DisplayData}. If a single

   * component registers multiple metadata items with the same key, only the most recent item will

   * be retained; previous versions are discarded.

   */

  @AutoValue

  public abstract static class Identifier implements Serializable {

    public abstract Path getPath();



    public abstract Class<?> getNamespace();



    public abstract String getKey();



    public static Identifier of(Path path, Class<?> namespace, String key) {

      return new AutoValue_DisplayData_Identifier(path, namespace, key);

    }



    @Override

    public String toString() {

      return String.format("%s%s:%s", getPath(), getNamespace(), getKey());

    }

  }



  /**

   * Structured path of registered display data within a component hierarchy.

   *

   * <p>Display data items registered directly by a component will have the {@link Path#root() root}

   * path. If the component {@link Builder#include includes} a sub-component, its display data will

   * be registered at the path specified. Each sub-component path is created by appending a child

   * element to the path of its parent component, forming a hierarchy.

   */

  public static class Path implements Serializable {

    private final ImmutableList<String> components;



    private Path(ImmutableList<String> components) {

      this.components = components;

    }



    /** Path for display data registered by a top-level component. */

    public static Path root() {

      return new Path(ImmutableList.of());

    }



    /**

     * Construct a path from an absolute component path hierarchy.

     *

     * <p>For the root path, use {@link Path#root()}.

     *

     * @param firstPath Path of the first sub-component.

     * @param paths Additional path components.

     */

    public static Path absolute(String firstPath, String... paths) {

      ImmutableList.Builder<String> builder = ImmutableList.builder();



      validatePathElement(firstPath);

      builder.add(firstPath);

      for (String path : paths) {

        validatePathElement(path);

        builder.add(path);

      }



      return new Path(builder.build());

    }



    /**

     * Hierarchy list of component paths making up the full path, starting with the top-level child

     * component path. For the {@link #root root} path, returns the empty list.

     */

    public List<String> getComponents() {

      return components;

    }



    /**

     * Extend the path by appending a sub-component path. The new path element is added to the end

     * of the path hierarchy.

     *

     * <p>Returns a new {@link Path} instance; the originating {@link Path} is not modified.

     */

    public Path extend(String path) {

      validatePathElement(path);

      return new Path(

          ImmutableList.<String>builder().addAll(components.iterator()).add(path).build());

    }



    private static void validatePathElement(String path) {

      checkNotNull(path);

      checkArgument(!"".equals(path), "path cannot be empty");

    }



    @Override

    public String toString() {

      StringBuilder b = new StringBuilder().append("[");

      Joiner.on("/").appendTo(b, components);

      b.append("]");

      return b.toString();

    }



    @Override

    public boolean equals(Object obj) {

      return obj instanceof Path && Objects.equals(components, ((Path) obj).components);

    }



    @Override

    public int hashCode() {

      return components.hashCode();

    }

  }



  /** Display data type. */

  public enum Type {

    STRING {

      @Override

      FormattedItemValue format(Object value) {

        return new FormattedItemValue(checkType(value, String.class, STRING));

      }

    },

    INTEGER {

      @Override

      FormattedItemValue format(Object value) {

        if (value instanceof Integer) {

          long l = ((Integer) value).longValue();

          return format(l);

        }



        return new FormattedItemValue(checkType(value, Long.class, INTEGER));

      }

    },

    FLOAT {

      @Override

      FormattedItemValue format(Object value) {

        return new FormattedItemValue(checkType(value, Number.class, FLOAT));

      }

    },

    BOOLEAN() {

      @Override

      FormattedItemValue format(Object value) {

        return new FormattedItemValue(checkType(value, Boolean.class, BOOLEAN));

      }

    },

    TIMESTAMP() {

      @Override

      FormattedItemValue format(Object value) {

        Instant instant = checkType(value, Instant.class, TIMESTAMP);

        return new FormattedItemValue(TIMESTAMP_FORMATTER.print(instant));

      }

    },

    DURATION {

      @Override

      FormattedItemValue format(Object value) {

        Duration duration = checkType(value, Duration.class, DURATION);

        return new FormattedItemValue(duration.getMillis());

      }

    },

    JAVA_CLASS {

      @Override

      FormattedItemValue format(Object value) {

        Class<?> clazz = checkType(value, Class.class, JAVA_CLASS);

        return new FormattedItemValue(clazz.getName(), clazz.getSimpleName());

      }

    };



    private static <T> T checkType(Object value, Class<T> clazz, DisplayData.Type expectedType) {

      if (!clazz.isAssignableFrom(value.getClass())) {

        throw new ClassCastException(

            String.format("Value is not valid for DisplayData type %s: %s", expectedType, value));

      }



      @SuppressWarnings("unchecked") // type checked above.

      T typedValue = (T) value;

      return typedValue;

    }



    /**

     * Format the display data value into a long string representation, and optionally a shorter

     * representation for display.

     *

     * <p>Internal-only. Value objects can be safely cast to the expected Java type.

     */

    abstract FormattedItemValue format(Object value);



    /**

     * Safe version of {@link Type#format(Object)}, which checks for null input value and if so

     * returns a {@link FormattedItemValue} with null value properties.

     *

     * @see #format(Object)

     */

    FormattedItemValue safeFormat(@Nullable Object value) {

      if (value == null) {

        return FormattedItemValue.NULL_VALUES;

      }



      return format(value);

    }



    @Nullable

    private static Type tryInferFrom(@Nullable Object value) {

      if (value instanceof Integer || value instanceof Long) {

        return INTEGER;

      } else if (value instanceof Double || value instanceof Float) {

        return FLOAT;

      } else if (value instanceof Boolean) {

        return BOOLEAN;

      } else if (value instanceof Instant) {

        return TIMESTAMP;

      } else if (value instanceof Duration) {

        return DURATION;

      } else if (value instanceof Class<?>) {

        return JAVA_CLASS;

      } else if (value instanceof String) {

        return STRING;

      } else {

        return null;

      }

    }

  }



  static class FormattedItemValue {

    /** Default instance which contains null values. */

    private static final FormattedItemValue NULL_VALUES = new FormattedItemValue(null);



    @Nullable

	private final Object shortValue;

    @Nullable

	private final Object longValue;



    private FormattedItemValue(@Nullable Object longValue) {

      this(longValue, null);

    }



    private FormattedItemValue(@Nullable Object longValue, @Nullable Object shortValue) {

      this.longValue = longValue;

      this.shortValue = shortValue;

    }



    Object getLongValue() {

      return this.longValue;

    }



    Object getShortValue() {

      return this.shortValue;

    }

  }



  private static class InternalBuilder implements Builder {

    private final Map<Identifier, Item> entries;

    private final Set<HasDisplayData> visitedComponents;

    private final Map<Path, HasDisplayData> visitedPathMap;



    @Nullable

	private Path latestPath;

    @Nullable

	private Class<?> latestNs;



    private InternalBuilder() {

      this.entries = Maps.newHashMap();

      this.visitedComponents = Sets.newIdentityHashSet();

      this.visitedPathMap = Maps.newHashMap();

    }



    @Override

    public Builder include(String path, HasDisplayData subComponent) {

      checkNotNull(subComponent, "subComponent argument cannot be null");

      checkNotNull(path, "path argument cannot be null");



      Path absolutePath = latestPath.extend(path);



      HasDisplayData existingComponent = visitedPathMap.get(absolutePath);

      if (existingComponent != null) {

        throw new IllegalArgumentException(

            String.format(

                "Specified path '%s' already used for "

                    + "subcomponent %s. Subcomponents must be included using unique paths.",

                path, existingComponent));

      }



      return include(absolutePath, subComponent);

    }



    @Override

    public Builder delegate(HasDisplayData component) {

      checkNotNull(component);



      return include(latestPath, component);

    }



    private Builder include(Path path, HasDisplayData subComponent) {

      if (visitedComponents.contains(subComponent)) {

        // Component previously registered; ignore in order to break cyclic dependencies

        return this;

      }



      // New component; add it.

      visitedComponents.add(subComponent);

      visitedPathMap.put(path, subComponent);

      Class<?> namespace = subComponent.getClass();

      // Common case: AutoValue classes such as AutoValue_FooIO_Read. It's more useful

      // to show the user the FooIO.Read class, which is the direct superclass of the AutoValue

      // generated class.

      if (namespace.getSimpleName().startsWith("AutoValue_")) {

        namespace = namespace.getSuperclass();

      }

      if (namespace.isSynthetic() && namespace.getSimpleName().contains("$$Lambda")) {

        try {

          namespace =

              Class.forName(namespace.getCanonicalName().replaceFirst("\\$\\$Lambda.*", ""));

        } catch (Exception e) {

          throw new PopulateDisplayDataException(

              "Failed to get the enclosing class of lambda " + subComponent, e);

        }

      }



      Path prevPath = latestPath;

      Class<?> prevNs = latestNs;

      latestPath = path;

      latestNs = namespace;



      try {

        subComponent.populateDisplayData(this);

      } catch (PopulateDisplayDataException e) {

        // Don't re-wrap exceptions recursively.

        throw e;

      } catch (Throwable e) {

        String msg =

            String.format(

                "Error while populating display data for component '%s': %s",

                namespace.getName(), e.getMessage());

        throw new PopulateDisplayDataException(msg, e);

      }



      latestPath = prevPath;

      latestNs = prevNs;



      return this;

    }



    /** Marker exception class for exceptions encountered while populating display data. */

    private static class PopulateDisplayDataException extends RuntimeException {

      PopulateDisplayDataException(String message, Throwable cause) {

        super(message, cause);

      }

    }



    @Override

    public Builder add(ItemSpec<?> item) {

      checkNotNull(item, "Input display item cannot be null");

      return addItemIf(true, item);

    }



    @Override

    public Builder addIfNotNull(ItemSpec<?> item) {

      checkNotNull(item, "Input display item cannot be null");

      return addItemIf(item.getValue() != null, item);

    }



    @Override

    public <T> Builder addIfNotDefault(ItemSpec<T> item, @Nullable T defaultValue) {

      checkNotNull(item, "Input display item cannot be null");

      ItemSpec<T> defaultItem = item.withValue(defaultValue);

      return addItemIf(!Objects.equals(item, defaultItem), item);

    }



    private Builder addItemIf(boolean condition, ItemSpec<?> spec) {

      if (!condition) {

        return this;

      }



      checkNotNull(spec, "Input display item cannot be null");

      checkNotNull(spec.getValue(), "Input display value cannot be null");



      if (spec.getNamespace() == null) {

        spec = spec.withNamespace(latestNs);

      }

      Item item = Item.create(spec, latestPath);



      Identifier id = Identifier.of(item.getPath(), item.getNamespace(), item.getKey());

      checkArgument(

          !entries.containsKey(id),

          "Display data key (%s) is not unique within the specified path and namespace: %s%s.",

          item.getKey(),

          item.getPath(),

          item.getNamespace());



      entries.put(id, item);

      return this;

    }



    private DisplayData build() {

      return new DisplayData(this.entries);

    }

  }



  /** Create a display item for the specified key and string value. */

  public static ItemSpec<String> item(String key, @Nullable String value) {

    return item(key, Type.STRING, value);

  }



  /** Create a display item for the specified key and {@link ValueProvider}. */

  public static ItemSpec<?> item(String key, @Nullable ValueProvider<?> value) {

    if (value == null) {

      return item(key, Type.STRING, null);

    }

    if (value.isAccessible()) {

      Object got = value.get();

      if (got == null) {

        return item(key, Type.STRING, null);

      }

      Type type = inferType(got);

      if (type != null) {

        return item(key, type, got);

      }

    }

    // General case: not null and type not inferable. Fall back to toString of the VP itself.

    return item(key, Type.STRING, String.valueOf(value));

  }



  /** Create a display item for the specified key and integer value. */

  public static ItemSpec<Integer> item(String key, @Nullable Integer value) {

    return item(key, Type.INTEGER, value);

  }



  /** Create a display item for the specified key and integer value. */

  public static ItemSpec<Long> item(String key, @Nullable Long value) {

    return item(key, Type.INTEGER, value);

  }



  /** Create a display item for the specified key and floating point value. */

  public static ItemSpec<Float> item(String key, @Nullable Float value) {

    return item(key, Type.FLOAT, value);

  }



  /** Create a display item for the specified key and floating point value. */

  public static ItemSpec<Double> item(String key, @Nullable Double value) {

    return item(key, Type.FLOAT, value);

  }



  /** Create a display item for the specified key and boolean value. */

  public static ItemSpec<Boolean> item(String key, @Nullable Boolean value) {

    return item(key, Type.BOOLEAN, value);

  }



  /** Create a display item for the specified key and timestamp value. */

  public static ItemSpec<Instant> item(String key, @Nullable Instant value) {

    return item(key, Type.TIMESTAMP, value);

  }



  /** Create a display item for the specified key and duration value. */

  public static ItemSpec<Duration> item(String key, @Nullable Duration value) {

    return item(key, Type.DURATION, value);

  }



  /** Create a display item for the specified key and class value. */

  public static <T> ItemSpec<Class<T>> item(String key, @Nullable Class<T> value) {

    return item(key, Type.JAVA_CLASS, value);

  }



  /**

   * Create a display item for the specified key, type, and value. This method should be used if the

   * type of the input value can only be determined at runtime. Otherwise, {@link HasDisplayData}

   * implementors should call one of the typed factory methods, such as {@link #item(String,

   * String)} or {@link #item(String, Integer)}.

   *

   * @throws ClassCastException if the value cannot be formatted as the given type.

   * @see Type#inferType(Object)

   */

  public static <T> ItemSpec<T> item(String key, Type type, @Nullable T value) {

    checkNotNull(key, "key argument cannot be null");

    checkNotNull(type, "type argument cannot be null");



    return ItemSpec.create(key, type, value);

  }

}